Skip to main content

Python 面向对象

面向对象发展史

自本章开始我们将进入Python的进阶学习阶段。如果在接下来的学习过程中感到有些困难,不用担心,这是正常现象。毕竟,我们不能永远只是输出“Hello, world”吧,学习就是要走出舒适区,进入学习区,但是步子不能迈得太大,否则容易进入恐慌区。

01三个区

对于进阶阶段,如果有难以理解的地方,可以多看几遍,多动手敲几遍代码,并与学习群中的小伙伴一起讨论,问题就会迎刃而解。好了,废话不多说,我们开始本章的课程:面向对象编程。首先需要说明的是,面向对象编程并不是一种编程语言,而是一种编程思想。

02面向对象编程

这种思想并非一开始就被提出,而是为了解决特定问题而诞生的。下面我们来了解一下面向对象编程的由来和发展。

在计算机发展早期,程序设计都是采用机器语言进行编写的,即直接使用二进制码表示机器能够识别和执行的指令。简单来说,就是直接编写0和1的序列来代表程序的语言,由机器直接执行。这个时代我们也戏称为“史前时代”。当时的程序员主要是女性,他们的工作方式是在波动机器上的开关,打开代表1,关闭代表0。程序的一系列指令都是通过这种开关操作完成的。如果今天完成了一个功能,明天需要再次完成同样的功能,那么就需要重复执行这些操作,因为无法保存。这些工作也只能手动完成。如下图:

03面向机器时代

后来人们发明了一种先进的方式,叫做打孔机。有了打孔机后,工作人员就不需要再按开关了,开关的动作直接在卡片上打孔来表示。如下图:

04打孔机

而且这些打孔带是可以保存的,今天执行过的打孔带明天可以再次执行,这大大提高了效率。但是由于机器语言编写太困难,于是发明了汇编语言,也称为符号语言,它使用助记符代替指令或操作数的地址,相比机器语言更方便一些,简化了编程过程,提高了程序的可读性,但本质上仍然是面向机器的语言,困难且容易出错,因此被称为一种低级语言。

为了解决面向机器编程的问题,计算机科学家们创建了一种叫做面向过程的语言,这被认为是一种高级语言。相对于机器语言,面向过程语言不再关注机器本身的操作指令,而是关注如何一步一步解决具体的问题。这种解决问题的过程,也就是面向过程的由来。与面向机器的思想相比,面向过程是一种思想上的飞跃,它将程序员从复杂的机器操作和运行细节中解放出来,主要关注具体需要解决的问题,大大减轻了程序员的负担,提高了工作效率。典型的面向过程语言有Fortran、Pascal以及著名的C语言。

然而,随着软件和计算机的发展,应用的复杂度越来越高,软件规模也越来越大,原有的程序开发方式已经不能满足需求,这就爆发了第一次软件危机。其中一个最典型的例子是IBM的OS360操作系统。当时有一个项目主管叫做布鲁克斯,他率领2000多个程序员,夜以继日的工作,共计花费了5000人一年的工作量,写出了将近100万行的代码,总投入超过5亿元,尽管投入如此之大,但项目进度却一再推迟,软件质量也得不到保障。 如下图所示:

05面向过程

在这个背景下,为了解决以上的这些问题,结构化设计程序作为另一种思想被提了出来。结构化设计主要特点是抛弃沟通语句,采用自顶向下逐步细化模块化的思想,将软件的复杂度控制在一定范围内,从而整体上降低了软件开发的复杂度。结构化程序思想成为了1970年代软件开发的潮流。

06结构化设计

然而,随着硬件的快速发展、业务需求的复杂化以及软件编程领域的广泛应用,第二次软件危机很快到来了。第一次软件危机体现在复杂度上,而第二次软件危机主要体现在可扩展性和可维护性上。面向过程方法已经越来越不适应快速多变的业务需求。因此,在这种背景下,面向对象的思想开始流行起来。面向对象的思想更贴近人类的思维方式,更脱离了机器思维,是软件思想上的一次飞跃。面向对象编程的语言有C++、Java以及Python等等。到目前为止,面向对象已经成为主流的开发思想。

07面向对象设计

但是,这并不意味着面向对象的设计就比面向过程的设计更高级、更先进。它们各有优劣。例如,C语言是面向过程编程语言,至今在编程语言排行榜中稳居前十位。因此,我们要根据具体的应用场景来选择不同的编程语言。而我们学习的Python语言既支持面向过程方式,又支持面向对象方式,可以根据自己的需求来选择不同的编程方式,甚至是多种编程方式同时使用。以上就是面向对象编程的发展历史。

面向对象vs面向过程

本节我们将介绍面向过程编程和面向对象编程之间的区别。面向过程是一种以事件为中心的编程思想,即将解决问题的步骤分析出来,然后一步一步地实现这些步骤。而面向对象则是以对象为中心的编程思想。如下图:

08面向对象和面向过程

在这里我们提到了“对象”,对象并非指男女朋友的对象,而是英文单词“object”。在Python中,一切皆为对象,包括人类、动物、植物,甚至碗和面等。面向对象的编程思想将要解决的问题分解成各个对象,并描述了这些对象在整个解决问题的步骤中的属性和行为。

09对象

举例来说,在前面我们介绍过“把大象装进冰箱”,这是一个顺序结构,运用了面向过程的思想。现在我们要做的是把大象装进冰箱,可以分成三个步骤:第一步是打开冰箱门,第二步是把大象装进去,第三步是把冰箱门关上。如果用程序来描述这个过程,可以概括为三个步骤:首先是定义一个“open”函数用来打开冰箱门,然后定义一个“run_in”函数用来把大象装进冰箱,最后定义一个“close”函数用来关上冰箱门。然后按照顺序依次调用这些函数,整个事件执行完毕。这种面向过程的方式按照事件发展的顺序一步一步来完成。如下图:

10面向过程编程

现在我们面临一个新的需求:将大熊装进冰箱。同样的情况下,冰箱里已经有一头大象。为了将大熊装进冰箱,首先需要将大象移出来,然后再将大熊放入。我们将通过程序的方式来实现这一功能。由于前面已经完成了将大象装进冰箱的过程,因此无需进行修改。接下来,我们需要调用open函数打开冰箱门。冰箱门打开后,我们需要将大象移出。我们将使用一个名为run_out的方法来实现将大象挪出冰箱的功能。完成了这些准备工作后,接着将大熊装入冰箱,然后关闭冰箱门。这便是整个流程。在这个过程中,我们新增了一个名为run_out的函数。在实现这个需求的过程中,我们发现对原有代码进行了大量修改,这意味着程序的可扩展性和可维护性并不理想。

11把大熊装进冰箱

接下来,我们将使用面向对象编程的思维方式来实现同样的功能。在面向对象的思想中,我们将以对象为核心,这个需求中有两个主要对象:大象和冰箱。这两个对象之间相互独立,大象可以移动进出,而冰箱门可以打开和关闭。通过这两个对象之间的相互协作,便可以实现将大象装进冰箱的功能。现在,让我们从程序的角度来看看如何实现这个功能。

首先,大象有两个方法:run_in(进入冰箱)和run_out(走出冰箱)。而冰箱有两个方法:open(打开冰箱门)和close(关闭冰箱门)。要实现将大象装进冰箱,首先冰箱需要调用open方法打开冰箱门,然后大象调用run_in方法进入冰箱。最后,冰箱对象调用close方法关闭冰箱门,这样就完成了这个功能。如果现在有相同的需求,要将大熊装进冰箱,我们可以将大象和大熊归为同一类别:动物。它们都具有run_inrun_out的方法,这样就实现了方法的重用。无论是大熊、大象还是其他动物,只要需要将它们装进冰箱或从冰箱中移出,都可以调用这两个方法。这便是面向对象编程的思想。如下图:

12面向对象编程

接下来,让我们总结一下面向过程编程和面向对象编程之间的优缺点。首先,面向过程编程的优点在于它能使编程任务流程化,使得实现方式和最终结果清晰可见。它的效率也较高,因为强调代码的简洁性和与数据结构的结合,从而开发出高效的程序。然而,它的缺点是需要深入思考,耗费精力;代码的重用性较低,扩展能力差,对于经常变更的需求,后期维护难度大。

13面向过程优点缺点

而面向对象编程的优点在于结构清晰,程序模块化和结构化,更符合人类的思维方式;易于扩展,代码的重用性高,可以设计出低耦合的系统;易于维护,由于系统低耦合,有利于减少后期维护工作量。然而,它的缺点在于开销较大,需要考虑对象内部的修改问题,增加了编程工作的负担和运行开销,使得程序显得臃肿。

14面向对象优缺点

面向过程编程和面向对象编程各有优劣,我们需要根据具体的需求来选择合适的编程方式。通常情况下,我们会将两种方式结合使用,以达到更好的效果。

类和实例

在本节课中,我们将深入介绍面向对象编程中的两个核心概念:类和实例。我们将从三个方面来介绍类和实例。首先,让我们先来了解什么是类。类这个词是由英文单词"class"翻译而来。有些人可能会认为"class"是指班级的意思,但其实它还有另一个动词的含义,即将事物进行分类。这种分类的概念与我们《战国策》中的的"人以群分,物以类聚"相符。

15战国策

人类在发展历史中喜欢将事物进行分类,因为这样有利于学习和认知。比如,我们将人按性别分为男性和女性,按年龄分为儿童、青年、壮年和老年等。这些分类的例子与面向对象中的类概念是一致的。因此,总结来说,类就是一组相似事物的统称,其中关键词包括"一组"(而不是一个),"相似"(而不是相同),"统称"(而不是名称)。如下图:

16类的概念

接下来我们来看几个类的例子。例如,如果说你和猪属于同一类,你可能会反驳说你和猪才是一类。

17猪

但实际上,你、我、他和猪都属于一类,因为我们都是哺乳动物。类似地,人类和植物也属于一类,因为它们都是生物。这表明如何对事物进行分类取决于我们所站的角度。

了解了类的概念后,我们再来看一下类的特性。我们以跑车和摩托车为例,如下图:

18跑车和摩托车

我们可以将它们归为车类,因为它们有着相似的特征,如轮子、油门、刹车、车灯和倒车镜。此外呢,他们还都能够停止、启动、加速、减速,这些都属于车这一类,在这里我将这些特性进行了区分,如果按照物理学中静态和动态的概念来说,轮子、油门、刹车、车灯和倒车镜,他们属于静态的,我们称之为属性,而停止、启动、加速、减速,他们属于动态的,我们称之为方法。

19方法和属性

我们再来看一个例子,一部苹果手机和一个实际的苹果。对于这两者如何成为同一类的事物?事实上,这取决于你所站的角度。如果你是一个商城系统的管理员,那么对于苹果手机和苹果来说,它们都是你的商品,因此可以将它们归类为商品类。那么它们都具有名称、价格、重量和库存等属性。这些共同的特征可以归结为所谓的属性。此外,我们可以对它们进行添加、删除和修改等操作。因此,这类特征被称为方法。由此可见,类由方法和属性组成。

20类的组成

介绍完了类之后,我们再来讲解什么是实例。实例一词源自英文单词"instance",有时也被称为实例对象。在Python中,一切皆为对象,比如字符串也是对象,因此你可以称之为字符串对象。那么实例或者实例对象有何作用呢?我们之前说过,类是一组相似事物的统称,它本身并不是具体的事物,因此我们不能直接使用它。比如说,我们有一个车类,但这个车类本身并不是一辆具体的车,因此我们无法直接使用它。那么,如何才能使用这个类呢?我们需要将其转化为某一类具体的事物,比如转化为一辆汽车,这辆汽车就是一个实例对象。我们可以对它进行实际的操作。另外,这个车类还可以生成摩托车,而摩托车也是一个实例对象,我们同样可以对它进行操作。总之,我们不能直接操作车类,但可以操作由车类生成的实例对象。

21实例对象

接下来,让我们再来看看商品类。同样地,商品类本身并不是一个商品,因此你不能直接使用它。我们需要使用这个商品类来生成一个具体的商品,比如一个苹果手机。此时,这个苹果手机就是一个实例对象。同样地,你也可以使用商品类来生成一个苹果,而这个苹果也是一个实例对象。因此,我们需要将一个类转化为一个在现实世界中可以看得见摸得着的具体事物时,就需要使用实例或实例对象。

接下来我们来讲解类和实例之间的关系。类本身不能直接使用,我们需要将其转化为一个实例后才能直接使用。举个例子,在生产月饼时,我们需要使用一个工具叫做月饼模具来制造月饼。如下图:

22月饼磨具

在这个例子中,月饼模具就相当于一个类,而最终生成的月饼就是实例对象。一个月饼模具可以制造出很多个月饼,因此类可以创造多个实例对象。类是一个抽象的概念,而实例则是一个具体的事物。通过类来生成实例对象的过程,我们称之为实例化。这就是类和实例之间的关系。如下图:

23实例化

创建类和实例

本节课我们将着重学习如何创建类和实例。在Python中,要创建一个类,其语法结构如下:

class ClassName(object):
pass

在这个语法结构中,class 是关键字,就像我们创建函数时使用 def 一样。这里创建类我们使用关键字 class。第二个部分是类名,在Python中创建类名需要遵循一定的方法,这个方法叫做大驼峰法。大驼峰法要求将每个单词的首字母进行大写,就像骆驼的驼峰一样,高的地方要大写,低的地方小写。如下图所示:

24大驼峰命名

例如,如果要创建一个商品订单表,命名为 goods order,则使用大驼峰法应该定义为 GoodsOrder。对于驼峰法还有一种叫做小驼峰法,它是指首字母要小写。但在Python中,类名需要使用大驼峰法,也就是每个单词的首字母大写。

类语法括号里面有一个 object,它是这个类默认的继承。在Python中,一切都是对象,包括类本身。所以,这个类继承自 object 这个对象。在这里,object 可以省略,因为它默认就是 object,甚至可以连括号也不写,直接是 class 类名:,但为了遵循编码规范,我们通常会将其填写上。

接下来冒号下面的就是类中的具体内容了。在这里,我们使用了一个 pass,代表着什么都没有。现在我们来开始编写代码,来创建一个类。创建类使用关键字 class,然后用大驼峰法命名一个类名。例如,我们创建一个名字叫做 Person 的类。括号里面,我们主动给它添加一个 object。对于这个 Person 类,现在我们还没想好怎么写,所以我们直接使用一个 passpass 代表什么都不写,它可以用来占位。创建好了一个类以后,我们并不能够直接使用这个类,我们需要将它实例化,转化为一个实例对象。实例化的过程非常简单,代码如下:

class Person(object):
pass
andy = Person()

直接使用 Person(),这个类名加一个括号,表示将这个类实例化了。实例化以后,它就生成了一个实例对象。通常,我们用一个变量来接收这个实例对象。例如,如果 Person 实例化以后变成了 Andy,那么 Andy 就是一个实例对象。通过 Person 类,经过这个实例化以后,就生成了一个实例对象。除此之外,Person 还可以生成多个实例对象。再给它复制一个变量,例如 Jack,这里就生成了两个实例对象。代码如下:

class Person(object):
pass
andy = Person()
jack = Person()

在一个Python文件中,我们可以创建多个类。接着我们创建第二个类,类名为 Animal。同样的,我们使用 object,并暂时使用 pass 来表示我们还没想好怎么写。然后我们实例化 Animal 这个类,类名加一个括号,表示实例化。然后将它赋值给 dog,同样的我们再创建一个 cat。代码如下:

# 定义一个名为 Person 的类,继承自 object
class Person(object):
pass

# 定义一个名为 Animal 的类,继承自 object
class Animal(object):
pass

# 实例化 Person 类,生成一个名为 andy 的实例对象
andy = Person()
# 实例化 Person 类,生成一个名为 jack 的实例对象
jack = Person()

# 实例化 Animal 类,生成一个名为 dog 的实例对象
dog = Animal()
# 实例化 Animal 类,生成一个名为 cat 的实例对象
cat = Animal()

现在我们就创建完了两个类,一个Person,一个Animal并且使用类创建了几个实例。

创建类方法

在上一节课中,我们学习了如何使用class关键字和类名创建类。然而我们创建的类没有任何内容。我们已经知道类由方法和属性组成,本节课我们将学习如何创建类的方法。创建类方法的语法结构如下:

25创建类的方法

首先是def关键字,类似于我们创建函数时的def。然后是方法名,方法名要求全小写,如果有多个单词,则用下划线分割。方法就是类中的函数,是实现类中行为的一部分。方法名的规则是全部小写,如果有多个单词,用下划线分隔。方法的第一个参数通常是self,它代表实例对象,不是一个关键字,你也可以使用其他名字,但通常使用self。其后的参数可以是零个或多个。

接下来我们来看如何在代码中创建类的方法。我们创建一个Python文件,命名为create_function。代码如下:

# 创建Animal类,继承自object类
class Animal(object):
# 定义eat方法
def eat(self):
# 打印正在吃的食物
print(f"正在吃饭")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")

现在我们创建了一个类,里面包含三个方法:eatplaysleep。创建完方法后,我们需要使用实例来调用这些方法。调用方法时,我们需要使用类名加括号来创建一个实例,然后将实例赋值给一个变量,例如dog,然后使用该实例来调用方法,方法调用的语法是实例名.方法名()。代码如下:

# 创建Animal类,继承自object类
class Animal(object):
# 定义eat方法
def eat(self):
# 打印正在吃的食物
print(f"正在吃饭")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")
dog = Animal()
dog.eat()
dog.play()
dog.sleep()

输出结果:

正在吃饭
正在玩耍
正在睡觉

这就是类方法的定义以及调用。

下面我们要为eat方法添加一个参数。我们将增加一个名为food的参数,用来表示正在吃的食物。代码如下:

# 创建Animal类,继承自object类
class Animal(object):
# 定义eat方法
def eat(self):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")
dog = Animal()
dog.eat("狗粮")
dog.play()
dog.sleep()

输出结果:

正在吃狗粮
正在玩耍
正在睡觉

随后再次运行程序,我们可以看到输出结果为“正在吃狗粮”,这里的狗粮被传递给了food参数。然后,print输出语句中的food变量会被替换为“狗粮”。这就是方法的创建和调用过程。

self的作用

在创建类方法时,必须传递一个参数,即self。本节课我们将介绍self究竟是什么,以及它的作用是什么。下面我们直接在代码中进行演示。我们创建一个Python文件,命名为self.py,并复制之前创建方法的代码。其中包含一个名为Animal的类,以及三个方法:eat、play和sleep。每个方法都带有一个参数self。代码如下:

# 创建Animal类,继承自object类
class Animal(object):
# 定义eat方法,接收一个参数food
def eat(self, food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")

# 创建Animal类的一个实例,即实例化
dog = Animal()
# 调用实例的play方法
dog.play()

当我们运行这段代码时,输出为“正在玩耍”,与我们预期一致。这是使用实例对象调用play方法的情况。

那么,能否使用类名来调用这个方法呢?我们尝试使用Animal.play进行调用,结果却报错了,提示我们需要一个位置参数self。如下图所示:

26使用类名调用方法

即使我们在Animal. play ()手动添加了self参数,仍然会报错,提示self没有定义。这是因为self变量根本没有被赋值。在方法中,self代表实例对象,而实例对象是通过类实例化获得的。所以在这里我们填写的是dog,表示这是一个实例对象。

27填写dog实例对象

此时就可以输出正在玩耍了,这也验证了我们这里的self,它就是一个实例对象。

接下来我们再创建一个实例对象,命名为pig,同样使用Animal()进行实例化。并将其中的dog改为pig。代码如下:

class Animal(object):
def eat(self,food):
print(f"正在吃{food}")

def play(self):
print("正在玩耍")

def sleep(self):
print("正在睡觉")

dog = Animal()
pig = Animal()
Animal.play(dog)
Animal.play(pig)

同样地,运行程序,可以正常输出结果。这再次证明了self就是一个实例对象。

接下来我们尝试调用eat函数,它除了有self参数外,还有另一个参数food。我们尝试使用类来调用它,即Animal.eat。结果提示我们eat需要两个参数:一个是self,一个是food。如下图所示:

28缺少两个参数

所以我们需要传递一个实例对象(dog或者pig),以及食物参数。例如,我们给它传递一个“狗粮”,代码如下:

# 创建Animal类,继承自object类
class Animal(object):
# 定义eat方法,接收一个参数food
def eat(self, food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")

# 创建Animal类的两个实例,即实例化
dog = Animal()
pig = Animal()

# 使用类的方式调用play方法,并传入实例对象dog
Animal.play(dog)
# 使用类的方式调用play方法,并传入实例对象pig
Animal.play(pig)

# 使用类的方式调用eat方法,需传入实例对象和食物参数
Animal.eat(dog, '狗粮')

然后运行程序,就可以正常输出“正在吃狗粮”的结果。通过以上两种方式可以看出,一个是通过实例对象来调用方法,另一个是通过类来调用方法。通过类来调用需要传递一个实例对象,因此相对繁琐。因此,通常情况下,我们会直接使用实例对象来调用类方法。

为什么实例对象可以调用方法呢?我们可以打印一下这个实例对象来看看它是什么。让我们尝试打印一下dog,结果显示它是一个Animal对象。如下图所示:

29打印dog查看实例对象

这个实例对象dog是Animal类的一个实例,所以它才能够调用这个类中定义的方法。如果调用一个不存在的方法,比如我调用dog.info,这个info在类中根本不存在,提示属性错误,因为Animal类并没有info方法。这再次验证了实例对象能够调用的只是类中定义好的方法,如果没有定义是不能够调用的。

接下来我们来学习实例对象还有什么作用呢,在实例化完成后,eat、play和sleep方法中的self都是相同的。我们可以交叉使用它们,就像在学习函数嵌套时,在一个函数内可以调用另一个函数一样。比如说,在睡觉之前,我想吃点东西,然后再睡觉,代码如下:

# 创建Animal类,继承自object类
class Animal(object):
# 定义eat方法,接收一个参数food
def eat(self, food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 调用eat方法,传入参数'狗粮',表示正在吃狗粮
self.eat('狗粮') # 相当于 dog.eat('狗粮')
# 打印正在睡觉
print("正在睡觉")

# 创建Animal类的一个实例,即实例化
dog = Animal()
# 调用实例的sleep方法
dog.sleep()

输出结果:

正在吃狗粮
正在睡觉

self.eat表示调用当前对象的eat方法。因为dog这个实例对象继承了Animal类,所以执行这行代码时会调用eat方法。然后输出正在睡觉。这证实了self实例对象的用处,它可以在每个方法中相互之间进行使用,因为它们都有self。方法之间的相互调用就和函数之间的嵌套调用是一样的,现在我们已经介绍完了self的作用。

__init__初始化方法

本节我们将介绍一个特殊的方法__init__初始化方法。为什么说它特殊呢?从其形式上可以看出,它与普通方法不同,因为其名称前后都有两个双下划线。通常,在定义方法时,我们使用小写字母,每个单词之间加一个下划线。而__init__方法不同,它在前后都添加了两个下划线。在Python中,对于这种形式的方法,我们称之为特殊方法,也有人称之为魔法方法或者是魔术方法。只要能够区分它与普通方法的不同即可。

30init初始化方法

接下来,我们将在代码中学习__init__方法的特殊之处。首先我们创建一个配置文件,命名为init,复制之前的代码。接下来我们定义__init__方法,同样使用关键字def,注意方法名称中的双下划线。它是Python中内置的方法。在这个方法中,我们输出一条信息:“我是动物”。代码如下:

# 定义一个Animal类,继承自object类
class Animal(object):
# 初始化方法,会在实例化对象时自动调用
def __init__(self):
print("我是动物")

# eat方法,接受一个参数food,用于描述动物正在吃什么
def eat(self, food):
print(f"正在吃{food}")

# play方法,描述动物正在玩耍
def play(self):
print(f"正在玩耍")

# sleep方法,描述动物正在睡觉
def sleep(self):
print("正在睡觉")

# 实例化一个Animal对象,赋值给变量dog
dog = Animal()

输出结果:

我是动物

为什么会这样呢?我们没有调用__init__方法,但它却自动输出了结果。显然在执行这行代码时,Python自动调用了__init__方法。如果我们注释掉这行代码,看看是否还会输出?结果没有任何输出。因此当Python实例化对象时,就调用了__init__方法。与普通方法不同的是,我们需要使用实例化对象来调用普通方法,才能执行相应代码。因此,__init__方法的特殊之处在于它在对象实例化时自动调用。

既然它叫初始化方法,它会初始化一些变量。现在我想为这个dog添加一个属性,dog.name,表示这个狗叫什么名字。假如说它叫泰迪。再调用play方法的时候,我想输出“泰迪正在玩耍”。代码如下:

# 定义一个Animal类,继承自object类
class Animal(object):
# 初始化方法,会在实例化对象时自动调用
def __init__(self):
print("我是动物")

# eat方法,接受一个参数food,用于描述动物正在吃什么
def eat(self, food):
print(f"正在{food}")

# play方法,描述动物正在玩耍,使用self.name输出动物的名字
def play(self):
print(f"{self.name}正在玩耍")

# sleep方法,描述动物正在睡觉
def sleep(self):
print("正在睡觉")

# 实例化一个Animal对象,赋值给变量dog
dog = Animal()
# 为dog对象添加一个名为name的属性,赋值为"泰迪"
dog.name = "泰迪"
# 调用dog的play方法,输出动物名字正在玩耍
dog.play()

输出结果:

我是动物
泰迪正在玩耍

结果输出我是动物,泰迪正在玩耍。这样我们就实现了这个功能。

那如果我再来实例化一个pig对象,代码如下:

# 定义一个Animal类,继承自object类
class Animal(object):
# 初始化方法,会在实例化对象时自动调用
def __init__(self):
print("我是动物")

# eat方法,接受一个参数food,用于描述动物正在吃什么
def eat(self, food):
print(f"正在{food}")

# play方法,描述动物正在玩耍,使用self.name输出动物的名字
def play(self):
print(f"{self.name}正在玩耍")

# sleep方法,描述动物正在睡觉
def sleep(self):
print("正在睡觉")

# 实例化一个Animal对象,赋值给变量dog
dog = Animal()
# 为dog对象添加一个名为name的属性,赋值为"泰迪"
dog.name = "泰迪"
# 调用dog的play方法,输出动物名字正在玩耍
dog.play()
pig = Animal()
pig.name = "小猪佩奇"
pig. play()

输出结果:

我是动物
泰迪正在玩耍
我是动物
小猪佩奇正在玩耍

结果一样的,第一个是dog泰迪,第二个是pig小猪佩奇。虽然我们这么写实现了这个功能,但是代码看起来有一些臃肿。如果它的属性比较多,就会重复很多次,这样非常的啰嗦。显然这种方式并不够Pythonic。那么我们要如何修改这段代码呢?这时我们就可以使用这里的__init__初始化方法来实现了。在初始化方法中,我们可以将实例对象的参数放到初始化方法里,也就是在self后面添加参数。注意self必须是第一个,这是位置参数,也就是说它占了这个位置,其它的参数必须放在后面。修改代码如下:

class Animal(object):
# 定义 Animal 类
def __init__(self, name):
# 初始化方法,接受参数 name
self.name = name
# 将参数 name 赋值给实例变量 self.name
print("我是动物")
# 打印输出提示信息 "我是动物"

def eat(self, food):
# 定义方法 eat,接受参数 food
print(f"正在{food}")
# 打印输出正在进食的信息,使用参数 food

def play(self):
# 定义方法 play
print(f"{self.name}正在玩耍")
# 打印输出正在玩耍的信息,引用实例变量 self.name

def sleep(self):
# 定义方法 sleep
print("正在睡觉")
# 打印输出正在睡觉的信息

dog = Animal("泰迪")
# 创建 Animal 类的实例对象 dog,并传入参数 "泰迪"
dog.play()
# 调用实例对象 dog 的 play() 方法

pig = Animal("小猪佩奇")
# 创建 Animal 类的另一个实例对象 pig,并传入参数 "小猪佩奇"
pig.play()
# 调用实例对象 pig 的 play() 方法

输出结果:

我是动物
泰迪正在玩耍
我是动物
小猪佩奇正在玩耍

看到输出结果是泰迪正在玩耍,小猪佩奇正在玩耍。所以说,这就是init方法的作用。初始化方法就是用来初始化一些变量。由于init方法会在实例化对象的同时调用像泰迪、小猪佩奇这些参数。那如果我们在实例化的时候,没有添加这个变量参数,我们看到提示一个TypeEorror,__init__方法缺少一个必要的位置参数name。所以说你在调用的时候就必须为这里传值了。这是一个参数的情况。

同理你也可以传递多个参数,比如说我在添加一个年龄参数,代码如下:

# 定义一个Animal类,继承自object类
class Animal(object):
# 初始化方法,接受两个参数name和age,用于初始化动物的名字和年龄
def __init__(self, name, age):
# 将传入的name赋值给实例属性self.name
self.name = name
# 将传入的age赋值给实例属性self.age
self.age = age
# 输出“我是动物”
print("我是动物")

# eat方法,接受一个参数food,用于描述动物正在吃什么
def eat(self, food):
print(f"正在{food}")

# play方法,描述动物正在玩耍,使用self.name和self.age输出动物的名字和年龄
def play(self):
print(f"{self.name}今年{self.age}岁,正在玩耍")

# sleep方法,描述动物正在睡觉
def sleep(self):
print("正在睡觉")

# 实例化一个Animal对象,参数为名字"泰迪"和年龄10,赋值给变量dog
dog = Animal("泰迪", 10)
# 调用dog的play方法,输出动物名字和年龄正在玩耍
dog.play()

# 实例化另一个Animal对象,参数为名字"小猪佩奇"和年龄5,赋值给变量pig
pig = Animal("小猪佩奇", 5)
# 调用pig的play方法,输出动物名字和年龄正在玩耍
pig.play()

输出结果:

我是动物
泰迪今年10岁,正在玩耍
我是动物
小猪佩奇今年5岁,正在玩耍

对于__init__初始化方法,这个方法是一个特殊方法,它之所以特殊,是因为程序在实例化的同时,会自动调用init方法。如下图所示:

31总结init

那么它如何初始化呢?就是在定义这个方法的时候需要初始化的参数,这些参数必须添加到self的后面,可以没有,也可以是多个。此外呢,还有一点需要注意的是,由于init初始化方法会优先运行,所以通常呢,我们会将init方法写在所有方法的最前面,也就是在类class定义的后面,直接跟着一个__init__初始化方法。

实例属性

属性分为两种:实例属性和类属性。本节将首先着重介绍实例属性。实际上,在之前的代码编写中,我们已经广泛地使用了实例属性。让我们在代码中再次审视实例属性。实例属性指的是与特定实例相关联的属性。我们可以通过实例来访问这些属性。我们首先创建一个新的 Python 文件。命名为instance_attribute.py,代码如下:

class Animal(object):
def __init__(self,name,age):
self.name = name
self.age = age
print("我是动物")

def eat(self,food):
print(f"正在{food}")

def play(self):
print(f"{self.name}今年{self.age}岁,正在玩耍")

def sleep(self):
print("正在睡觉")

dog = Animal("泰迪",10)
dog.play()
pig = Animal("小猪佩奇",5)
pig.play()

比如,在 __init__() 初始化方法中,我们已经使用了实例属性,因为 self 表示的是当前实例。在这里,我们将 name 赋值给 self.name,这里的 name 就是一个实例属性。类似地,age 也是一个实例属性。既然是实例属性,我们可以通过实例来调用它们。例如,在 play 方法中,我们使用 self.name 就调用了实例属性。值得一提的是,Python 是一门动态编程语言,我们可以动态地为对象添加属性。例如,我们创建了一个名为 dog 的实例对象,然后为其添加了一个名为 gender 的属性,并给它赋值为 "男"。这样,这个属性就成功添加了。代码如下:

class Animal(object):
def __init__(self,name,age):
self.name = name
self.age = age
print("我是动物")

def eat(self,food):
print(f"正在{food}")

def play(self):
print(f"{self.name}今年{self.age}岁,正在玩耍")

def sleep(self):
print("正在睡觉")

dog = Animal("泰迪",10)
dog.play()
dog.gender = "男"
pig = Animal("小猪佩奇",5)
pig.play()

我们可以使用 dir() 函数来检查对象的所有属性和方法。这个函数会返回一个列表,其中每个元素都是对象的属性或方法。代码如下:

class Animal(object):
def __init__(self,name,age):
self.name = name
self.age = age
print("我是动物")

def eat(self,food):
print(f"正在{food}")

def play(self):
print(f"{self.name}今年{self.age}岁,正在玩耍")

def sleep(self):
print("正在睡觉")

dog = Animal("泰迪",10)
dog.play()
dog.gender = "男"
print (dir(dog))

输出结果:

我是动物
泰迪今年10岁,正在玩耍
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'eat', 'gender', 'name', 'play', 'sleep']

在运行代码后,我们可以看到返回的列表中包含了对象的属性和方法。属性名前的双下划线表示特殊方法,即 Python 内置的一些方法。在这个列表中,我们可以看到 age 是一个属性,eat 是一个方法,而 gender 则是动态添加的属性。这表明它已经成功地添加到对象中。当拥有了这些属性和方法之后,dog 对象就能够调用它们。比如说,我们可以输出 print dog.gender 来查看 dog 对象的性别属性,结果将会是 "男"。

接下来让我们再来看一下 pig 对象。同样地,使用 dir() 函数来查看它有哪些属性和方法。输出结果如下:

我是动物
泰迪今年10岁,正在玩耍
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'eat', 'gender', 'name', 'play', 'sleep']
我是动物
小猪佩奇今年5岁,正在玩耍
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'eat', 'name', 'play', 'sleep']

在上面我们看到了 dog 对象的属性和方法,在下面我们将会看到 pig 对象的属性和方法。这些以双下划线开头的特殊方法对于两个对象都是相同的。然而,我们主要关注的是最后一项,即从 age 开始是我们自己定义的属性。我们可以对比一下两个对象的属性列表。在 dog 对象的列表中多了一个 gender 属性。这是因为 gender 是针对 dog 对象单独定义的,因此只有 dog 对象能够调用它。尝试用 pig 对象调用 gender 属性将会导致属性错误,因为 pig 对象并没有这个属性。

这就是实例属性的主要应用。首先,实例属性可以在类内部进行定义,通常是在初始化方法 __init__() 中定义。同时,也可以在外部动态地进行定义,就像我们在示例中所做的那样。对于实例属性,我们可以在每个方法中通过 self. 的形式来调用它们。以上就是实例属性的主要内容。

类属性(类变量)

类属性在类中定义,类似于变量,因此有时也称为类变量。在本节中,我们将学习类属性的概念以及为何要使用它们。首先让我们看一段代码示例。如下图所示:

33类变量实例代码

在代码中有一个名为 Animal 的类。在初始化方法 __init__() 之外,我们定义了一个变量 age,并将其值设置为 5。这个变量与之前使用的实例属性有所不同。实例属性通常在 __init__() 初始化方法中定义,例如 name。通过将值赋给实例对象 self,我们可以使 self 拥有一个名为 name 的属性。而 age 则在类的内部,在其他函数的外部,类似于一个普通变量。那么类属性有什么作用呢?接下来我们将在代码中演示如何使用这些类属性。

我们创建一个名为 class_attribute.py 的 Python 文件,然后复制之前的代码。代码如下:

# 创建Animal类,继承自object类
class Animal(object):
# 定义eat方法
def eat(self,food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")
dog = Animal()
dog.eat("狗粮")
dog.play()
dog.sleep()

在这个代码中,有一个 Animal 类和三个方法。我们之前提到,在使用方法之前,通常需要先将类实例化,生成一个实例化对象,然后直接调用方法。现在我们创建一个类属性,比如名字叫做 age,并赋值为 5。我们可以看到,实例对象可以调用类中的方法。那么它们能否调用类属性呢?我们输出 dog.age,需要使用 dog.age 这种形式。代码如下:

# 创建Animal类,继承自object类
class Animal(object):
age = 5
# 定义eat方法
def eat(self,food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")
dog = Animal()
dog.eat("狗粮")
dog.play()
dog.sleep()
print(dog.age)

输出结果:

正在吃狗粮
正在玩耍
正在睡觉
5

结果显示为 5。这表明使用实例对象是可以调用类属性的。然后创建另一个实例对象,称为 pig,接下来,我们使用 pig.age 输出 pigage,代码如下:

# 创建Animal类,继承自object类
class Animal(object):
age = 5
# 定义eat方法
def eat(self,food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")
dog = Animal()
print(dog.age)
pig = Animal()
print(pig.age)

输出结果:

5
5

结果也是 5。这说明了一个道理:在实例化对象中,每一个实例化对象都可以调用其所在类的属性,并且它们的值都是相同的。换句话说,类属性对于每一个实例对象都是通用的。

接下来我们来看一下,直接使用类对象能否调用它们。如果要调用类的属性,直接使用 Animal.age,然后我们输出一下,结果还是 5。这说明对于一个类属性,它自身也是可以直接调用的。除此之外,由它实例化生成的对象,比如 dogpig,它们也可以直接调用类属性。

现在让我们总结一下。如下图所示:

34age属性

有一个 Animal 类,它有一个 age 属性,这个属性是类属性,当前我们给它赋了一个值为 5。然后我们使用 Animal 类生成了几个实例,首先生成了一个 dog 实例,它也有一个 age 属性,值为 5,因为 dog 是由 Animal 生成的。然后我们又创建了一个实例对象 pig,它也会继承 Animal 中的属性值,因此其值也是 5。但是,如果 dog 实例或者 pig 实例它们自身也有一个 age 属性,那么此时 age 它的值应该是多少呢?我们可以为这个对象动态地添加一个属性 dog.age,并给它赋一个值,比如 10。代码如下:

# 创建Animal类,继承自object类
class Animal(object):
age = 5
# 定义eat方法
def eat(self,food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")
dog = Animal()
dog.age = 10
print(dog.age)

pig = Animal()
print(pig.age)
print (Animal.age)

输出结果:

10
5
5

那么此时 dog 对象就不会继承 Animal 的属性了,因为它自身已经有了,它会优先选择自身的 age 属性。而 pig 对象自身没有 age 属性,所以它会继承 Animal 中的属性,其值还是 5。

接下来,如果我们将 Animal 的属性 age 由 5 改为 8,代码如下:

# 创建Animal类,继承自object类
class Animal(object):
age = 5
# 定义eat方法
def eat(self,food):
# 打印正在吃的食物
print(f"正在吃{food}")

# 定义play方法,无参数
def play(self):
# 打印正在玩耍
print("正在玩耍")

# 定义sleep方法,无参数
def sleep(self):
# 打印正在睡觉
print("正在睡觉")
Animal.age = 8
dog = Animal()
dog.age = 10
print(f'dog的age是{dog.age}')

pig = Animal()
print(f'pig的age是{pig.age}')
print(f'Animal的age是{Animal.age}')

输出结果:

dog的age是10
pig的age是8
Animal的age是8

那么此时 dog.age 它依然还会使用自身的 age 值,即 10,而 pigage 则会指向 Animal 这个父类的属性,也就是 8。这就是类属性的特点,即实例对象它们可以共享类的属性。

类的继承

在之前的章节中,我们已经学习了类的基本概念,包括方法和属性。我们介绍了两种方法,一种是普通方法,另一种是双下划线的特殊方法,即 init 初始化方法。对于属性,我们介绍了实例属性和类属性,它们是类中的关键组成部分。

35类和类的关系

本节我们要重点介绍类与类之间的关系,即类的继承。一提到继承,很多人会首先想到财产的继承,这两个继承的意思是相似的。在家族族谱中,如下图所示:

36族谱

我们看到有祖父、祖母、外公、外婆、爸爸、妈妈和我。我可以继承爸爸和妈妈的财产,而爸爸的兄弟可以继承祖父和祖母的财产,妈妈和舅舅则可以继承外公和外婆的财产。这是现实生活中继承的一个例子。在 Python 中,类的继承与现实生活中的继承基本类似。我们将展示在 Python 中如何使用继承。如下图所示:

37继承语法结构

在这里,我们定义一个类的语法结构,类名后面跟着括号里的 object。现在,我们要对这个类使用继承,将 object 改为父类的名称。值得注意的是,括号里的这个就是父类,有时也被称为基类,而我们定义的这个类则被称为子类。子类可以继承父类或基类。

举个例子,在这里我们有三个类:Animal 类、Dog 类和 Pig 类。如下图所示:

38继承类

Dog 类继承了 AnimalPig 类也同样继承了 Animal。这时,Animal 类就被称为父类,而 DogPig 这两个类就是子类。子类可以继承父类的方法和属性,即 Dog 类可以继承 play 方法和 sleep 方法,同样 Pig 类也可以。

接下来,我们将在代码中演示如何使用类的继承。我们创建一个文件名为 class_inheritance.py,来展示如何使用类的继承。首先,我们创建一个类,将其视为父类 Animal。在这个类中,我们设置为 object,然后添加一个初始化方法 __init__(),并添加一个参数 name,将其赋值给 self.name,从而创建一个实例属性。接着我们定义了 eat 方法,直接输出 self.name,表示正在吃东西;然后是 play 方法,输出正在玩耍;最后是 sleep 方法,输出正在睡觉。现在我们创建了这个类,其中包含了 4 个方法。代码如下:

class Animal(object):
def __init__(self,name):
self.name = name

def eat(self):
print(f"{self.name}正在吃东西")

def play(self):
print(f"{self.name}正在玩耍")

def sleep(self):
print(f"{self.name}正在睡觉")

接下来,我们定义两个子类:DogPig。同样地,使用 class 关键字,类名为 Dog,我们将其基类设置为 Animal,表示它继承了 Animal 类。在 Dog 类中,我们定义了一个方法 bark,输出小狗会汪汪叫。类似地,对于 Pig 类,也是继承了 Animal 类,并定义了一个方法,输出小猪会咘咘叫。代码如下:

class Animal(object):
def __init__(self,name):
self.name = name

def eat(self):
print(f"{self.name}正在吃东西")

def play(self):
print(f"{self.name}正在玩耍")

def sleep(self):
print(f"{self.name}正在睡觉")

class Dog(Animal):
def bark(self):
print(f"小狗会汪汪叫")

class Pig(Animal):
def buu(self):
print(f"小猪会咘咘叫")

现在我们来看一下子类的特征。我们先实例化一个 Dog 类,然后输出 dog,代码如下:

# 定义 Animal 类,它是一个顶级类,包含初始化方法 __init__() 和普通方法 eat()、play()、sleep()
class Animal(object):
def __init__(self, name): # 初始化方法接收一个参数 name,并将其赋值给实例属性 self.name
self.name = name

def eat(self): # 普通方法 eat() 输出实例名正在吃东西
print(f"{self.name}正在吃东西")

def play(self): # 普通方法 play() 输出实例名正在玩耍
print(f"{self.name}正在玩耍")

def sleep(self): # 普通方法 sleep() 输出实例名正在睡觉
print(f"{self.name}正在睡觉")

# 定义 Dog 类,它继承自 Animal 类
class Dog(Animal):
def bark(self): # 定义 bark() 方法,输出小狗会汪汪叫
print(f"小狗会汪汪叫")

# 定义 Pig 类,它也继承自 Animal 类
class Pig(Animal):
def buu(self): # 定义 buu() 方法,输出小猪会咘咘叫
print(f"小猪会咘咘叫")

dog = Dog() # 实例化 Dog 类
print(dog) # 打印实例对象 dog

结果显示了一个错误信息,提示缺少参数 name。这是因为 Dog 类继承了父类 Animal 的初始化方法,所以在实例化时需要添加一个参数 name。我们给它赋值为“腊肠狗”,代码如下:

# 定义 Animal 类,它是一个顶级类,包含初始化方法 __init__() 和普通方法 eat()、play()、sleep()
class Animal(object):
def __init__(self, name): # 初始化方法接收一个参数 name,并将其赋值给实例属性 self.name
self.name = name

def eat(self): # 普通方法 eat() 输出实例名正在吃东西
print(f"{self.name}正在吃东西")

def play(self): # 普通方法 play() 输出实例名正在玩耍
print(f"{self.name}正在玩耍")

def sleep(self): # 普通方法 sleep() 输出实例名正在睡觉
print(f"{self.name}正在睡觉")

# 定义 Dog 类,它继承自 Animal 类
class Dog(Animal):
def bark(self): # 定义 bark() 方法,输出小狗会汪汪叫
print(f"小狗会汪汪叫")

# 定义 Pig 类,它也继承自 Animal 类
class Pig(Animal):
def buu(self): # 定义 buu() 方法,输出小猪会咘咘叫
print(f"小猪会咘咘叫")

dog = Dog("腊肠狗") # 实例化 Dog 类
print(dog) # 打印实例对象 dog

输出结果:

<_main_.Dog object at 0x1044bef50>

这时就可以正常输出了,结果显示为 Dog 类的实例对象。既然子类可以继承父类的初始化方法,那么它能否继承父类的普通方法呢?我们尝试调用 dog.eat 方法,结果能够正常输出,说明子类可以继承父类的方法。接着,我们尝试添加一个属性 age,赋值为 10,然后输出 dog.age,结果显示为 10,这也说明了子类可以继承父类的属性。代码如下:

# 定义 Animal 类,它是一个顶级类,包含初始化方法 __init__() 和普通方法 eat()、play()、sleep()
class Animal(object):
age = 10
def __init__(self, name): # 初始化方法接收一个参数 name,并将其赋值给实例属性 self.name
self.name = name

def eat(self): # 普通方法 eat() 输出实例名正在吃东西
print(f"{self.name}正在吃东西")

def play(self): # 普通方法 play() 输出实例名正在玩耍
print(f"{self.name}正在玩耍")

def sleep(self): # 普通方法 sleep() 输出实例名正在睡觉
print(f"{self.name}正在睡觉")

# 定义 Dog 类,它继承自 Animal 类
class Dog(Animal):
def bark(self): # 定义 bark() 方法,输出小狗会汪汪叫
print(f"小狗会汪汪叫")

# 定义 Pig 类,它也继承自 Animal 类
class Pig(Animal):
def buu(self): # 定义 buu() 方法,输出小猪会咘咘叫
print(f"小猪会咘咘叫")

dog = Dog("腊肠狗") # 实例化 Dog 类
print(dog) # 打印实例对象 dog
dog.eat()
print(dog.age)

输出结果:

<__main__.Dog object at 0x101a5fe50>
腊肠狗正在吃东西
10

至于实例属性,我们在实例化 dog 时,Dog 类中有一个 bark 方法,可以调用实例属性 self.name。我们试着调用 dog.bark 方法,代码如下:

# 定义 Animal 类,它是一个顶级类,包含初始化方法 __init__() 和普通方法 eat()、play()、sleep()
class Animal(object):
age = 10
def __init__(self, name): # 初始化方法接收一个参数 name,并将其赋值给实例属性 self.name
self.name = name

def eat(self): # 普通方法 eat() 输出实例名正在吃东西
print(f"{self.name}正在吃东西")

def play(self): # 普通方法 play() 输出实例名正在玩耍
print(f"{self.name}正在玩耍")

def sleep(self): # 普通方法 sleep() 输出实例名正在睡觉
print(f"{self.name}正在睡觉")

# 定义 Dog 类,它继承自 Animal 类
class Dog(Animal):
def bark(self): # 定义 bark() 方法,输出小狗会汪汪叫
print(f"{self.name}会汪汪叫")

# 定义 Pig 类,它也继承自 Animal 类
class Pig(Animal):
def buu(self): # 定义 buu() 方法,输出小猪会咘咘叫
print(f"小猪会咘咘叫")

dog = Dog("腊肠狗") # 实例化 Dog 类
print(dog) # 打印实例对象 dog
dog.eat()
print(dog.age)
dog.bark()

输出结果:

<__main__.Dog object at 0x10312ffd0>
腊肠狗正在吃东西
10
腊肠狗会汪汪叫

结果正常输出,“腊肠狗会汪汪叫”,这也说明了子类同样会继承父类的实例属性。以上就是 Python 中类继承的特性。

方法的重写

在上一节课程中,我们学习了类继承的基本概念。当子类继承父类后,它可以调用父类的方法和属性。如果在子类中也包含相同的方法,如下图所示:

39方法的重写

比如在Dog子类和Pig子类中都继承了Animal类,它们同样都有一个eat方法,那么当这个子类调用eat方法时,是调用子类的eat方法还是调用父类的eat方法呢?这就是我们本节要介绍的方法的重写。

下面我们在代码中演示如何使用方法的重写。我们创建一个Python文件,命名为rewrite_function,然后复制之前的代码。在代码中,我们定义了一个父类Animal,并且定义了两个子类DogPig。在dog类中,我们创建了一个方法eat,输出实例的名字加上“正在吃狗粮”的信息。接着我们实例化Dog类,将其赋值给dog对象。然后调用dog.eat方法,代码如下:

class Animal(object):
age = 10 # 定义类属性age为10

def __init__(self, name): # 定义初始化方法,接收一个name参数
self.name = name # 将参数name赋值给实例属性self.name

def eat(self): # 定义吃东西的方法
print(f"{self.name}正在吃东西") # 输出实例名字正在吃东西的信息

def play(self): # 定义玩耍的方法
print(f"{self.name}正在玩耍") # 输出实例名字正在玩耍的信息

def sleep(self): # 定义睡觉的方法
print(f"{self.name}正在睡觉") # 输出实例名字正在睡觉的信息


class Dog(Animal): # 定义子类Dog,继承自父类Animal

def eat(self): # 重写父类的吃东西的方法
print(f"{self.name}正在吃狗粮") # 输出实例名字正在吃狗粮的信息

def bark(self): # 定义狗叫的方法
print(f"{self.name}会汪汪叫") # 输出实例名字会汪汪叫的信息


class Pig(Animal): # 定义子类Pig,继承自父类Animal

def buu(self): # 定义猪叫的方法
print(f"小猪会咘咘叫") # 输出小猪会咘咘叫的信息


dog = Dog("腊肠狗") # 实例化一个Dog对象,传入参数"腊肠狗"
dog.eat() # 调用实例的吃东西的方法

输出结果:

腊肠狗正在吃狗粮

结果显示“腊肠狗正在吃狗粮”,也就是调用了子类自身的eat方法。这种现象非常好理解,因为子类的方法是在子类自身定义的,所以优先会调用子类自身的方法。只有在子类自身没有定义该方法时,才会去调用父类的方法。因为子类重写了父类的方法,这种现象被称为方法的重写。

为什么要使用方法的重写呢?因为父类中通常定义的是一个通用的方法,它要适用于每一个子类。但是,某些子类可能有特殊的需求,比如Dog类吃狗粮,Pig类吃猪粮,这时候我们就需要在子类中重写该方法,以适应特定子类的需求。对于其他方法,如果在子类中没有重写,我们仍然可以直接调用,输出的结果仍然是父类中定义的结果。这就是继承中方法的重写。

调用父类_init_()初始化方法

在前一节课中,我们讨论了方法的重写,即如果子类和父类中存在同名的方法,子类中的方法会覆盖父类中的方法。在子类继承父类后,子类对象的实例化会自动调用父类的初始化方法。然而,如果子类自身也有一个初始化方法,那么它会覆盖父类中的初始化方法。那么,如果我们想要在子类中保留自己的初始化方法,同时还想调用父类中的初始化方法,该如何实现呢?

我们可以在这个方法重写的代码示例中解决这个问题,代码如下:

class Animal(object):
age = 10 # 定义类属性age为10

def __init__(self, name): # 定义初始化方法,接收一个name参数
self.name = name # 将参数name赋值给实例属性self.name

def eat(self): # 定义吃东西的方法
print(f"{self.name}正在吃东西") # 输出实例名字正在吃东西的信息

def play(self): # 定义玩耍的方法
print(f"{self.name}正在玩耍") # 输出实例名字正在玩耍的信息

def sleep(self): # 定义睡觉的方法
print(f"{self.name}正在睡觉") # 输出实例名字正在睡觉的信息


class Dog(Animal): # 定义子类Dog,继承自父类Animal

def eat(self): # 重写父类的吃东西的方法
print(f"{self.name}正在吃狗粮") # 输出实例名字正在吃狗粮的信息

def bark(self): # 定义狗叫的方法
print(f"{self.name}会汪汪叫") # 输出实例名字会汪汪叫的信息


class Pig(Animal): # 定义子类Pig,继承自父类Animal

def buu(self): # 定义猪叫的方法
print(f"小猪会咘咘叫") # 输出小猪会咘咘叫的信息


dog = Dog("腊肠狗") # 实例化一个Dog对象,传入参数"腊肠狗"
dog.eat() # 调用实例的吃东西的方法

在Dog子类中定义了一个eat方法,这会覆盖父类的方法。在实例化子类时,我们需要传递一个参数,我们传递了一个"腊肠狗"。这是因为在父类的初始化方法中,我们传递了一个name参数,所以在实例化对象时,我们也要传递这个参数。

现在,我们在子类中也定义了一个初始化方法。显然,这个方法会覆盖父类的方法,因为它们具有相同的名称。但是,如果我们既想保留子类中的初始化方法,又想调用父类中的初始化方法,应该怎么做呢?我们保留父类的初始化方法的名称,并在其中添加一个gender参数,表示性别。然后,将其转换为实例属性gender。代码如下:

class Animal(object):
age = 10 # 定义类属性age为10

def __init__(self, name): # 定义初始化方法,接收一个name参数
self.name = name # 将参数name赋值给实例属性self.name

def eat(self): # 定义吃东西的方法
print(f"{self.name}正在吃东西") # 输出实例名字正在吃东西的信息

def play(self): # 定义玩耍的方法
print(f"{self.name}正在玩耍") # 输出实例名字正在玩耍的信息

def sleep(self): # 定义睡觉的方法
print(f"{self.name}正在睡觉") # 输出实例名字正在睡觉的信息


class Dog(Animal): # 定义子类Dog,继承自父类Animal
def __init__(self, gender):
self.gender = gender

def eat(self): # 重写父类的吃东西的方法
print(f"{self.name}正在吃狗粮") # 输出实例名字正在吃狗粮的信息

def bark(self): # 定义狗叫的方法
print(f"{self.name}会汪汪叫") # 输出实例名字会汪汪叫的信息


class Pig(Animal): # 定义子类Pig,继承自父类Animal

def buu(self): # 定义猪叫的方法
print(f"小猪会咘咘叫") # 输出小猪会咘咘叫的信息


dog = Dog("腊肠狗") # 实例化一个Dog对象,传入参数"腊肠狗"
dog.eat() # 调用实例的吃东西的方法

结果显示了一个错误,指出dog对象没有name属性。如下图所示:

40没有name属性

这是因为在子类中并没有name属性,但是我们却在这里调用了name属性。那么,如何才能获取父类中的name属性呢?这时我们需要使用一个特殊的方法,称为supersuper一词意味着上级,通过super方法,我们可以调用Animal类的init方法,代码如下:

class Animal(object):
age = 10 # 定义类属性age为10

def __init__(self, name): # 定义初始化方法,接收一个name参数
self.name = name # 将参数name赋值给实例属性self.name

def eat(self): # 定义吃东西的方法
print(f"{self.name}正在吃东西") # 输出实例名字正在吃东西的信息

def play(self): # 定义玩耍的方法
print(f"{self.name}正在玩耍") # 输出实例名字正在玩耍的信息

def sleep(self): # 定义睡觉的方法
print(f"{self.name}正在睡觉") # 输出实例名字正在睡觉的信息


class Dog(Animal): # 定义子类Dog,继承自父类Animal
def __init__(self,name,gender):
super().__init__(name)
self.gender = gender

def eat(self): # 重写父类的吃东西的方法
print(f"{self.name}正在吃狗粮") # 输出实例名字正在吃狗粮的信息

def bark(self): # 定义狗叫的方法
print(f"{self.name}会汪汪叫") # 输出实例名字会汪汪叫的信息


class Pig(Animal): # 定义子类Pig,继承自父类Animal

def buu(self): # 定义猪叫的方法
print(f"小猪会咘咘叫") # 输出小猪会咘咘叫的信息


dog = Dog("腊肠狗","male") # 实例化一个Dog对象
dog.eat() # 调用实例的吃东西的方法

输出结果:

腊肠狗正在吃狗粮

总结一下,当子类和父类中同时存在初始化方法时,子类中的初始化方法会覆盖父类的初始化方法,这与覆盖普通方法的原理相同。如果我们想要保留父类中的初始化方法,可以使用super函数。super函数表示调用其上级,也就是其父类Animal类。然后,使用点号调用init初始化方法。在init方法中,如果有参数,就需要通过参数接收值。这个值通过参数在实例化时传递给初始化方法。这样我们就同时保留了自身的初始化方法,也保证了父类的初始化方法。